<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h3 data-lake-id="kIIiW" id="kIIiW"><span data-lake-id="u9b1732aa" id="u9b1732aa">背景</span></h3>
  <p data-lake-id="u1cbbffaf" id="u1cbbffaf"><br></p>
  <p data-lake-id="uffee9456" id="uffee9456"><span data-lake-id="u6623811c" id="u6623811c">这种背景的话一般都是在分布式场景中，需要保证各个系统之间的数据的最终一致性，比如交易下单环节，保证订单系统和用户积分系统之间的最终一致。</span></p>
  <p data-lake-id="ucc2e484f" id="ucc2e484f"><span data-lake-id="u5142355a" id="u5142355a">​</span><br></p>
  <p data-lake-id="u5d47f285" id="u5d47f285"><span data-lake-id="u0afab945" id="u0afab945">也就是说，用户下单后，订单需要创建成功，用户积分也要增加成功，如果有一个失败了，这都是没满足一致性。</span></p>
  <p data-lake-id="u1a82706f" id="u1a82706f"><span data-lake-id="ud3de658b" id="ud3de658b">​</span><br></p>
  <h3 data-lake-id="Jz35o" id="Jz35o"><span data-lake-id="u641e0b24" id="u641e0b24">技术选型</span></h3>
  <p data-lake-id="uea4f0438" id="uea4f0438"><br></p>
  <p data-lake-id="u0da8d169" id="u0da8d169"><span data-lake-id="ub2300a0b" id="ub2300a0b">保证分布式事务的方案有很多，比如本地消息表、MQ的事务消息、TCC、Seata等、2PC等</span></p>
  <p data-lake-id="ufe0582b5" id="ufe0582b5"><span data-lake-id="ub84ff359" id="ub84ff359">​</span><br></p>
  <p data-lake-id="u8ec4c7df" id="u8ec4c7df"><span data-lake-id="uab367c1b" id="uab367c1b">这些方案中各自都有优缺点，首先比较重的就是TCC、Seata和2PC，因为他们要么需要引入一个单独的协调者，要么需要代码做改造，要么对分布式系统之间有很强的侵入性。</span></p>
  <p data-lake-id="u55901b6a" id="u55901b6a"><span data-lake-id="ub6aed8f9" id="ub6aed8f9">​</span><br></p>
  <p data-lake-id="u3b4cca07" id="u3b4cca07"><span data-lake-id="u76792c5f" id="u76792c5f">比如TCC需要下游提供Try、Confirm和Cancel三种操作，2PC也是，需要把一个业务操作拆成2个阶段。</span></p>
  <p data-lake-id="u002a645e" id="u002a645e"><span data-lake-id="u639fe286" id="u639fe286">​</span><br></p>
  <p data-lake-id="ud1f1bb06" id="ud1f1bb06"><span data-lake-id="udad05a3c" id="udad05a3c">那么相对来说比较轻量级的方案就是</span><strong><span data-lake-id="u2fc21a9c" id="u2fc21a9c">依赖可靠消息，实现最终一致性</span></strong><span data-lake-id="ue91da854" id="ue91da854">。尤其是我们这个场景中，积分的增加其实不需要强一致性，只需要保证几秒钟之后积分增加成功就行，而且是一旦下单成功，积分增加必须成功，所以就比较适合使用可靠消息来保证最终一致性。</span></p>
  <p data-lake-id="ua24cd1a2" id="ua24cd1a2"><span data-lake-id="u53b799bd" id="u53b799bd">​</span><br></p>
  <p data-lake-id="u9e8e74a1" id="u9e8e74a1"><span data-lake-id="u79e5aac5" id="u79e5aac5">那么也就是说我们可以在创订单系统创建订单成功之后，发一个MQ消息，然后积分系统接收这个MQ消息即可。</span></p>
  <p data-lake-id="u29b3d965" id="u29b3d965"><span data-lake-id="u46e4206b" id="u46e4206b">​</span><br></p>
  <pre lang="java"><code>
@Transactional
public void order(OrderDTO orderDTO){

	orderServive.createOrder(orderDTO);
	mqService.send(orderDTO);
	
}
</code></pre>
  <p data-lake-id="ub102cd94" id="ub102cd94"><br></p>
  <p data-lake-id="ua74b7519" id="ua74b7519"><span data-lake-id="ud2df65e9" id="ud2df65e9">但是这个方案存在一个问题，那就是第二步，发送消息其实是有可能失败的。那么就有以下几种情况：</span></p>
  <p data-lake-id="ub5b9fab3" id="ub5b9fab3"><span data-lake-id="uabf2e879" id="uabf2e879">​</span><br></p>
  <p data-lake-id="uba5ec892" id="uba5ec892"><span data-lake-id="u60091096" id="u60091096">1、消息发送失败，MQ没接到消息</span></p>
  <p data-lake-id="u54e4b6ec" id="u54e4b6ec" style="text-indent: 2em"><span data-lake-id="uf51d40d2" id="uf51d40d2">这种情况，</span><code data-lake-id="u6ae47e5d" id="u6ae47e5d"><span data-lake-id="u2b5a5136" id="u2b5a5136">messageService.send(orderDTO);</span></code><span data-lake-id="u546e19ab" id="u546e19ab">会抛异常，那么本地事务捕获到这个异常之后，把createOrder回滚了就行了</span></p>
  <p data-lake-id="ue9e7cc3f" id="ue9e7cc3f" style="text-indent: 2em"><span data-lake-id="ub3b50a48" id="ub3b50a48">​</span><br></p>
  <p data-lake-id="u867d4dfb" id="u867d4dfb"><span data-lake-id="u06d05764" id="u06d05764">2、MQ接到了消息，但是客户端因为网络延迟以为失败了</span></p>
  <p data-lake-id="udde5c124" id="udde5c124" style="text-indent: 2em"><span data-lake-id="u05336489" id="u05336489">这种情况比较复杂了，就是说客户端因为接到了失败的response，会直接回滚createOrder。但是MQ收到了消息之后，会投递给积分系统，积分系统会直接消费消息，然后增加积分</span></p>
  <p data-lake-id="u3f901c02" id="u3f901c02" style="text-indent: 2em"><span data-lake-id="u7f341088" id="u7f341088">​</span><br></p>
  <p data-lake-id="uda7b674d" id="uda7b674d" style="text-indent: 2em"><span data-lake-id="u576f0913" id="u576f0913">​</span><br></p>
  <p data-lake-id="ua47609a3" id="ua47609a3"><span data-lake-id="ued105e1e" id="ued105e1e">上面的第二种情况，就导致了数据不一致。</span></p>
  <p data-lake-id="ub0eef919" id="ub0eef919"><span data-lake-id="ue9f2b996" id="ue9f2b996">​</span><br></p>
  <p data-lake-id="u326cc6dc" id="u326cc6dc"><span data-lake-id="u73cb3438" id="u73cb3438">那么想要解决这个问题，要么就是用MQ的事务消息，要么就是引入本地消息表。因为不是所有的MQ都支持事务消息，所以这里我们选择本地消息表。</span></p>
  <p data-lake-id="u4d4bfaaf" id="u4d4bfaaf"><span data-lake-id="ud4334cf8" id="ud4334cf8">​</span><br></p>
  <h3 data-lake-id="kubu8" id="kubu8"><span data-lake-id="uc1f5f49c" id="uc1f5f49c">具体实现</span></h3>
  <p data-lake-id="ubf816c10" id="ubf816c10"><br></p>
  <p data-lake-id="u744f9e81" id="u744f9e81"><span data-lake-id="u9d39d640" id="u9d39d640">在这个环节中，在订单服务的数据库中创建一张本地消息表</span></p>
  <p data-lake-id="ue9e070f4" id="ue9e070f4"><span data-lake-id="u4b5a36af" id="u4b5a36af">​</span><br></p>
  <table data-lake-id="Eaw7P" id="Eaw7P" margin="true" width-mode="contain" class="lake-table" style="width: 750px">
   <colgroup>
    <col width="250">
    <col width="250">
    <col width="250">
   </colgroup>
   <tbody>
    <tr data-lake-id="u1ac09706" id="u1ac09706">
     <td data-lake-id="u70a8bb10" id="u70a8bb10">
      <p data-lake-id="u7a4fed40" id="u7a4fed40"><span data-lake-id="uddab8cd1" id="uddab8cd1">id</span></p></td>
     <td data-lake-id="ue3bcf908" id="ue3bcf908">
      <p data-lake-id="uaf137b6a" id="uaf137b6a"><span data-lake-id="ud32c2820" id="ud32c2820">long</span></p></td>
     <td data-lake-id="u62271d5a" id="u62271d5a"></td>
    </tr>
    <tr data-lake-id="u446b283c" id="u446b283c">
     <td data-lake-id="u09a56ae8" id="u09a56ae8">
      <p data-lake-id="ufb5c0e98" id="ufb5c0e98"><span data-lake-id="ucaa460fe" id="ucaa460fe">gmt_create</span></p></td>
     <td data-lake-id="u3e1801bd" id="u3e1801bd">
      <p data-lake-id="ucbc97573" id="ucbc97573"><span data-lake-id="uc5f3529c" id="uc5f3529c">datetime</span></p></td>
     <td data-lake-id="ub4a8c99b" id="ub4a8c99b"></td>
    </tr>
    <tr data-lake-id="u48de4dd8" id="u48de4dd8">
     <td data-lake-id="ud786b92a" id="ud786b92a">
      <p data-lake-id="ud0f8ae33" id="ud0f8ae33"><span data-lake-id="ua061b59d" id="ua061b59d">gmt_modified</span></p></td>
     <td data-lake-id="u56feb9c9" id="u56feb9c9">
      <p data-lake-id="u6acce9b1" id="u6acce9b1"><span data-lake-id="u932f8e5b" id="u932f8e5b">datetime</span></p></td>
     <td data-lake-id="u661e50d2" id="u661e50d2"></td>
    </tr>
    <tr data-lake-id="uf9e9de3f" id="uf9e9de3f">
     <td data-lake-id="u11666756" id="u11666756">
      <p data-lake-id="u97b94a4d" id="u97b94a4d"><span data-lake-id="u3f42de51" id="u3f42de51">message_type</span></p></td>
     <td data-lake-id="u3b180e44" id="u3b180e44">
      <p data-lake-id="u8421c8c2" id="u8421c8c2"><span data-lake-id="u26a15c47" id="u26a15c47">varchar</span></p></td>
     <td data-lake-id="u70bf7dec" id="u70bf7dec"></td>
    </tr>
    <tr data-lake-id="uad7b4b86" id="uad7b4b86">
     <td data-lake-id="u93a45fed" id="u93a45fed">
      <p data-lake-id="ua52347a5" id="ua52347a5"><span data-lake-id="uf1f3d6d7" id="uf1f3d6d7">biz_type</span></p></td>
     <td data-lake-id="u83927318" id="u83927318">
      <p data-lake-id="u822db757" id="u822db757"><span data-lake-id="u050386fe" id="u050386fe">varchar</span></p></td>
     <td data-lake-id="ua5629aa6" id="ua5629aa6"></td>
    </tr>
    <tr data-lake-id="u20b5ee5e" id="u20b5ee5e" style="height: 37px">
     <td data-lake-id="u2b588282" id="u2b588282">
      <p data-lake-id="u80dc5078" id="u80dc5078"><span data-lake-id="u9ff163f1" id="u9ff163f1">identifier</span></p></td>
     <td data-lake-id="uaef417a9" id="uaef417a9">
      <p data-lake-id="u3a244bec" id="u3a244bec"><span data-lake-id="u08a5f92b" id="u08a5f92b">varchar</span></p></td>
     <td data-lake-id="u5823ddef" id="u5823ddef"></td>
    </tr>
    <tr data-lake-id="u80896f69" id="u80896f69">
     <td data-lake-id="uedc46b72" id="uedc46b72">
      <p data-lake-id="u6728b058" id="u6728b058"><span data-lake-id="u217a88c8" id="u217a88c8">content</span></p></td>
     <td data-lake-id="u994214a1" id="u994214a1">
      <p data-lake-id="u3511c60f" id="u3511c60f"><span data-lake-id="uded4b89b" id="uded4b89b">text</span></p></td>
     <td data-lake-id="u20009188" id="u20009188"></td>
    </tr>
    <tr data-lake-id="uc37db4ec" id="uc37db4ec">
     <td data-lake-id="uf0f27a23" id="uf0f27a23">
      <p data-lake-id="u004393fe" id="u004393fe"><span data-lake-id="u5d024717" id="u5d024717">state</span></p></td>
     <td data-lake-id="u91464bd9" id="u91464bd9">
      <p data-lake-id="u5d1d7618" id="u5d1d7618"><span data-lake-id="u9733c445" id="u9733c445">varchar</span></p></td>
     <td data-lake-id="u700d8acb" id="u700d8acb"></td>
    </tr>
   </tbody>
  </table>
  <p data-lake-id="u5137bb58" id="u5137bb58"><br></p>
  <p data-lake-id="u8b98eb58" id="u8b98eb58"><span data-lake-id="u6dbb0335" id="u6dbb0335">这样表就用来记录本地消息的。这样我们就可以把以上代码做一下调整：</span></p>
  <p data-lake-id="u6ffe00a1" id="u6ffe00a1"><span data-lake-id="ufe68ec40" id="ufe68ec40">​</span><br></p>
  <pre lang="java"><code>
@Transactional
public void order(OrderDTO orderDTO){

	orderServive.createOrder(orderDTO);
	messageService.createMessage(orderDTO);
	
}
</code></pre>
  <p data-lake-id="ud456ad5c" id="ud456ad5c"><br></p>
  <p data-lake-id="u070d1dd2" id="u070d1dd2"><span data-lake-id="u1454a8f2" id="u1454a8f2">这样我们就在一个事务中，创建两条数据库记录，因为加了事务，那么就可以保证，如果order创建成功，message也一定能写入成功。否则就都失败。</span></p>
  <p data-lake-id="u254efe6c" id="u254efe6c"><span data-lake-id="u4bdb1a11" id="u4bdb1a11">​</span><br></p>
  <p data-lake-id="u4d207bb7" id="u4d207bb7"><span data-lake-id="u8670bb2f" id="u8670bb2f">但是这里只是记录了本地消息，还需要把本地消息通过MQ发出去。这里就可以有很多办法了，一种是异步扫表，还可以直接同步发消息，也可以借助Spring Event来异步处理，都是可以的。</span></p>
  <p data-lake-id="u6f274ef6" id="u6f274ef6"><span data-lake-id="uff07b73f" id="uff07b73f">​</span><br></p>
  <p data-lake-id="u825ebb58" id="u825ebb58"><span data-lake-id="ud834b8b6" id="ud834b8b6">但是如果是同步发的话时效性肯定更好，但是同步发消息需要注意，要把调MQ发消息的地方放到事务外，要不然会因为MQ网络延迟等问题导致回滚，就又出现前面的问题了。</span></p>
  <p data-lake-id="ua6d42982" id="ua6d42982"><span data-lake-id="uebe87774" id="uebe87774">​</span><br></p>
  <p data-lake-id="u76c1d9e5" id="u76c1d9e5"><span data-lake-id="u147f668a" id="u147f668a">所以就可以用编程式事务：</span></p>
  <p data-lake-id="u8e592238" id="u8e592238"><span data-lake-id="u9dc5def9" id="u9dc5def9">​</span><br></p>
  <pre lang="java"><code>
@Autowired
TransactionTemplate transactionTemplate;

public void order(OrderDTO orderDTO){

   transactionTemplate.execute(
        new TransactionCallbackWithoutResult(){  
            @Override
            public Object doTransactionWithoutResult(TransactionStatus status){  
                orderServive.createOrder(orderDTO);
								messageService.createMessage(orderDTO);
            }  
    }); 
    
	mqService.send(orderDTO);
	messageService.updateSuccess(orderDTO);
	
}
</code></pre>
  <p data-lake-id="u2d04ef40" id="u2d04ef40"><br></p>
  <pre lang="java"><code>
@Autowired
TransactionTemplate transactionTemplate;

public void order(OrderDTO orderDTO){

   boolean transactionSuccess = transactionTemplate.execute(new TransactionCallback&lt;Boolean&gt;() {
        @Override
        public Boolean doInTransaction(TransactionStatus status) {
            try {
                orderServive.createOrder(orderDTO);
                messageService.createMessage(orderDTO);
                //以上执行如果未抛异常，则成功，返回true
                return true; // 表示事务执行成功
            } catch (Exception e) {
                // 如果发生异常，则标记事务为回滚
                status.setRollbackOnly();
                return false; // 表示事务执行失败
            }
        }
    });

    if (transactionSuccess) {
        // 事务执行成功，可以执行 mqService.send(orderDTO)
        mqService.send(orderDTO);
        messageService.updateSuccess(orderDTO);
    } else {
        // 事务执行失败的处理逻辑
        // 可以抛出异常或记录日志等
    }
}

</code></pre>
  <p data-lake-id="uef8cf4bd" id="uef8cf4bd"><br></p>
  <p data-lake-id="u2ddb93bc" id="u2ddb93bc"><span data-lake-id="u7b9b8b82" id="u7b9b8b82">在事务中写入本地业务数据+本地消息，然后在事务外发MQ消息，如果发送失败了，也不影响事务的commit，如果发送成功了，把本地消息表的状态推进一下。</span></p>
  <p data-lake-id="ud0707b8c" id="ud0707b8c"><span data-lake-id="u09b087d9" id="u09b087d9">​</span><br></p>
  <p data-lake-id="uade1d555" id="uade1d555"><span data-lake-id="ucf51b663" id="ucf51b663">如果失败，下一次再通过定时任务扫表把需要处理的事件查出来重发就行了。</span></p>
  <p data-lake-id="u6a4e1d2c" id="u6a4e1d2c"><span data-lake-id="u1f046281" id="u1f046281">​</span><br></p>
  <p data-lake-id="u22f58059" id="u22f58059"><span data-lake-id="u7de87f2d" id="u7de87f2d">所以本地消息表中还需要有一个定时任务，还需要提供一个接口给下游回调，具体的主要流程看这个就行了：</span></p>
  <p data-lake-id="u15aa5eb0" id="u15aa5eb0"><br></p>
  <h3 data-lake-id="e51e79c6" id="e51e79c6"><span data-lake-id="u757dc255" id="u757dc255">学习资料</span></h3>
  <p data-lake-id="u1bc59ff9" id="u1bc59ff9"><br></p>
  <p data-lake-id="u5a6486e4" id="u5a6486e4"><br></p>
  <p data-lake-id="u9e5854b6" id="u9e5854b6"><br></p>
  <p data-lake-id="uacf1e615" id="uacf1e615"><br></p>
  <p data-lake-id="u2d2aed81" id="u2d2aed81"><br></p>
  <p data-lake-id="u3229ff71" id="u3229ff71"><br></p>
 </body>
</html>